package coms6111.astbbw.webdb;

import java.io.*;
import java.net.URISyntaxException;
import java.util.ArrayList;

import org.apache.log4j.*;

import coms6111.astbbw.QueryFetcher;
import coms6111.astbbw.ResultParser;
import coms6111.astbbw.ResultSet;

public class Classifier {

	final static private String YAHOOID = "JAcfc9HIkY2GjpQ4i33DdD6imUDJGzOSTwV6gGXrVUwcUvroKiW6";

	/**
	 * The website to be classified.
	 */
	public String website;
	/**
	 * A Logger object for all methods in this class.
	 */
	private Logger l = Logger.getLogger(this.getClass());

	public int t_coverage;
	public double t_specificity;


	public Classifier(String webs, double specificity, int coverage) {
		this.website=webs;
		this.t_coverage=coverage;
		this.t_specificity=specificity;
	}

	/**
	 * @param nodename
	 * @return
	 * @throws IOException
	 * @throws URISyntaxException
	 */
	Classification classifyNode(String nodename) throws IOException, URISyntaxException {
		return classifyNode(nodename, null,1.0);
	}

	Classification classifyTree() throws IOException, URISyntaxException {
		Classification c = classifyNode("Root",null,1.0);
		c.classifyChildren();
		return c;
	}

	/**
	 * @param nodename
	 * @param sample
	 * @return
	 * @throws IOException
	 * @throws URISyntaxException
	 */
	protected Classification classifyNode(String nodename, DocumentSample parent, double parentSpec) throws IOException, URISyntaxException {
		String queries[][] = readQueryStrings(nodename);
		if (null == queries) return null;
		DocumentSample sample;
		if (null == parent ) {
			sample = new DocumentSample();
		} else {
			sample = new DocumentSample(parent);
		}
		ArrayList<String> subtreeNames = new ArrayList<String>();
		ArrayList<Integer> subtreeCounts = new ArrayList<Integer>();

		String currentSubtree = null;
		QueryFetcher q = new QueryFetcher(YAHOOID, this.website);
		ResultParser rp = new ResultParser();
		for (int i = 0; i < queries.length; i++ ) {
			String stName = queries[i][0];
			String query  = queries[i][1];
			if ( !stName.equals(currentSubtree) ) {
				currentSubtree = stName;
				subtreeNames.add(currentSubtree);
				subtreeCounts.add(0);
			}
			l.debug("Running query '" + query + "'");
			String result = q.runQuery(query);
			ResultSet rs = rp.addResult(result);
			int results_found = rs.totalHits;
			l.debug("Got " + results_found + " hits");
			sample.addPages(rs);
			int old_results = subtreeCounts.get(subtreeCounts.size() - 1);
			subtreeCounts.set(subtreeCounts.size() - 1, results_found + old_results);

		}
		return new Classification(subtreeNames, subtreeCounts, sample, parentSpec);
	}


	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Logger.getRootLogger().setLevel(Level.INFO);
		BasicConfigurator.configure();

		Logger l = Logger.getLogger(Classifier.class);

		if(args.length!=3){
			System.out.println("Requires three arguments-- Website, Threshold Specificity and Threshold Covereage ");
			l.fatal("user error: aborting");
		}
		else{
			double specificity=Double.parseDouble(args[1]);
			int coverage=Integer.parseInt(args[2]);
			l.info("Running classifier for " + args[0]);

			Classifier classifier=new Classifier(args[0],specificity, coverage);//will be frm args

			Classification c ;
			try {
				c = classifier.classifyTree();
			} catch (Exception e) {
				l.fatal("Error running classifier", e);
				throw new RuntimeException(e);
			}
			String treepaths[][] = c.getFinalClassification();

			System.out.println("The final classification is:");

			if (treepaths.length > 0 ) {
				for (int i = 0; i < treepaths.length; i++) {
					System.out.print("Root");
					for(int j = 0; j < treepaths[i].length; j++) {
						System.out.print("/");
						System.out.print(treepaths[i][j]);
					}
					System.out.print('\n');
				}
			} else {
				System.out.println("Root");
			}

			try {
				c.sample.updateAll();
				c.printSamples();
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * Read the query strings for the classifier for a given tree node.
	 * @param node the lower-case name of the current tree node (e.g. 'root')
	 * @return the contents of the relevant query-list file, as an array of strings
	 * @throws IOException
	 */
	private static String[][] readQueryStrings(String node) throws IOException {
		Logger l = Logger.getLogger(Classifier.class);
		ArrayList<String[]> got = new ArrayList<String[]>();
		String resourceName = "/" + node.toLowerCase() + ".txt";
		InputStream rs = Classifier.class.getResourceAsStream(resourceName);
		if (null == rs)
			return null;
		BufferedReader b = new BufferedReader(new InputStreamReader(rs));
		String tmp;
		while (null != (tmp = b.readLine())) {
			l.trace("Got this line: " + tmp);
			int space_at = tmp.indexOf(' ');
			String row[] = new String[2];
			row[0] = tmp.substring(0, space_at);
			row[1] = tmp.substring(space_at + 1);
			l.trace("subtree: '" + row[0] + "'; query: '" + row[1] + "'");
			got.add(row);
		}

		return got.toArray(new String[0][2]);
	}




	/**
	 * Class to encapsulate classification results.
	 * @author Ben Warfield
	 *
	 */
	class Classification {

		private String stNames[];
		private Integer stCounts[];
		private double stSpecificities[];
		protected Classification[] children;
		protected DocumentSample sample;
		protected double parentSpec = 1;

		/**
		 * Build a new classification result from the given arraylists of subtree names and
		 * hit counts.
		 * @param stNames_in
		 * @param stCounts_in
		 */
		public Classification(ArrayList<String> stNames_in, ArrayList<Integer> stCounts_in) {
			stNames = stNames_in.toArray(new String[0]);
			this.stCounts = stCounts_in.toArray(new Integer[0]);
			int total_count = 0;
			for (int i = 0; i < stCounts.length; i++) {
				total_count += stCounts[i];
			}
			stSpecificities = new double[stCounts.length];
			for (int i = 0; i < stCounts.length; i++) {
				if (0 < total_count) {
					stSpecificities[i] = (double) stCounts[i] / total_count;
				} else {
					stSpecificities[i] = 0;
				}
			}
			children = new Classification[stCounts.length];
		}

		public Classification(ArrayList<String> stNames_in, ArrayList<Integer> stCounts_in,
				DocumentSample s) {
			this(stNames_in, stCounts_in);
			this.sample = s;
		}


		/**
		 * @param stNames
		 * @param stSpecificities
		 * @param sample
		 * @param parentSpec
		 */
		private Classification(ArrayList<String> stNames_in, ArrayList<Integer> stCounts_in, DocumentSample sample, double parentSpec) {
			this(stNames_in,stCounts_in,sample);
			this.parentSpec = parentSpec;
		}

		public void classifyChildren() throws IOException, URISyntaxException {
			/*
			 * for each child node (stNames)
			 * if specificity/coverage are high enough
			 * 		if child node can be further subdivided
			 *	 		classify child node
			 */
			for (int stNum = 0; stNum < stNames.length; stNum++) {
				String name = stNames[stNum];
				double spec = stSpecificities[stNum] * this.parentSpec;
				int coverage = stCounts[stNum];
				Classifier this_classifier = Classifier.this;
				Classification childClass = null;
				if ( this_classifier.t_specificity <= spec && this_classifier.t_coverage <= coverage ) {
					l.debug("classifying child node " + name);
					childClass = this_classifier.classifyNode(name,this.sample,spec);
					if (null != childClass) {
						childClass.classifyChildren();
					}
				} else {
					l.debug("Child node " + name + " does not require sub-classification");
				}
				children[stNum] = childClass;
			}
		}

		/**
		 * @return the name of the subtree with the highest specificity.
		 */
		public String getBestSubtree() {
			String bestSubtree = null;
			double bestSpec = -1;
			for (int i = 0; i < this.stNames.length; i++) {
				if (stSpecificities[i] > bestSpec) bestSubtree = stNames[i];
			}
			return bestSubtree;
		}

		/**
		 * Find all subtrees with specificity and coverage that meet our current cutoffs.
		 * @param cutoffSpec the minimum specificity for a subtree to qualify.
		 * @param cutoffCount the minimum coverage (result count) for a subtree to qualify.
		 * @return the names of all subtrees that meet the given thresholds
		 */
		public String[] qualifiedSubtrees(double cutoffSpec, int cutoffCount) {
			ArrayList<String> found = new ArrayList<String>();
			for (int i = 0; i < this.stCounts.length; i++) {
				String stn = stNames[i];
				if ( stCounts[i] >= cutoffCount
						&& this.stSpecificities[i]*this.parentSpec >= cutoffSpec) {
					l.debug("Subtree '" + stn + "' meets cutoffs");
					found.add(stn);
				} else {
					l.debug("Subtree '" + stn + "' does not meet cutoffs");
				}
			}
			return found.toArray(new String[0]);
		}

		/**
		 * Find the specificity for a given subtree.
		 * @param subtree the name of the subtree to check (e.g. "Computers")
		 * @return the calculated specificity value, or -1 if there is no such value
		 */
		public double specFor(String subtree) {
			for (int i = 0; i < this.stNames.length; i++ ) {
				if ( subtree.equals(this.stNames[i])) {
					return this.stSpecificities[i] * parentSpec;
				}
			}
			return -1; // XXX or raise exception?
		}

		/**
		 * @return
		 */
		public String[][] getFinalClassification() {
			double specCutoff = Classifier.this.t_specificity;
			int countCutoff = Classifier.this.t_coverage;
			// for each subtree
			ArrayList<String[]> answer = new ArrayList<String[]>();
			for (int i = 0; i < this.stCounts.length; i++) {
				// if the subtree meets the cutoffs...
				String thisName = this.stNames[i];
				int thisCount = this.stCounts[i];
				double thisSpec = this.stSpecificities[i] * this.parentSpec;
				if ( thisCount < countCutoff || thisSpec < specCutoff) continue;
				String childResults[][] = null;
				if (null != children[i]) {
					 childResults = children[i].getFinalClassification();
				}
				if (null == childResults || 0 == childResults.length) {
					// ...  then it gets added
					String tmp[] = {thisName};
					answer.add(tmp);
				} else {
					// ... unless it has children, in which case it gets added with them appended
					for (int j = 0; j < childResults.length; j++) {
						String[] subpath = childResults[j];
						ArrayList<String> path = new ArrayList<String>();
						path.add(thisName);
						for (int k = 0; k < subpath.length; k++) path.add(subpath[k]);
						answer.add(path.toArray(new String[0]));
					}
				}
			}
			return answer.toArray(new String[0][0]);
		}

		public void printSamples() throws IOException {
			String website = Classifier.this.website;
			String baseFormat = "sample-%s-%s.txt";
			Object args[] = {"Root",website};
			DocumentSample.printSample(
					sample.getDocumentFrequencies(),
					String.format(baseFormat, args)
			);
			for(int i = 0; i < children.length; i++) {
				if (null != children[i]) {
					args[0] = stNames[i];
					String filename = String.format(baseFormat, args);
					DocumentSample.printSample(
							children[i].sample.getDocumentFrequencies(), filename
					);
				}
			}
		}
	}
}
